package raytracer;

import game.Consts;

import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;

import scene.Color;
import scene.DirectionalLight;
import scene.PointLight;
import scene.Scene;
import scene.SceneLoader;
import scene.SpotLight;

public class Raytracer implements Runnable {
	public enum RaytracerState {
		IDLE, BUSY
	};

	private BufferedImage image = null;
	private Scene scene;
	private int x1, x2;
	private boolean done;

	private boolean useJitteredSuperSampling = false;
	private int superSamplingValue = 2;

	private LinkedList<Ray> primaryRays;
	private Iterator<Ray> primaryRaysIterator;

	private RaytracerState state = RaytracerState.IDLE;

	private Random rnd;

	public Raytracer(Scene scene, int x1, int x2, boolean jittered, int jVal) {
		this.scene = scene;
		this.rnd = new Random();
		this.done = false;
		this.x1 = x1;
		this.x2 = x2;
		this.useJitteredSuperSampling = jittered;
		this.superSamplingValue = jVal;
		this.primaryRays = new LinkedList<Ray>();
		for (int x = x1; x <= x2; x++) {
			for (int y = 0; y < scene.renderInfo.height; y++) {
				this.primaryRays.add(new Ray());
			}
		}
		System.out.println("Raytracer from " + x1 + " to " + x2
				+ " (inclusive)");
	}

	public Raytracer(String inputFilename, int x1, int x2, boolean jittered,
			int jVal) {
		this.rnd = new Random();
		this.done = false;
		this.x1 = x1;
		this.x2 = x2;
		this.useJitteredSuperSampling = jittered;
		this.superSamplingValue = jVal;
		this.scene = SceneLoader.loadScene(inputFilename);
		this.primaryRays = new LinkedList<Ray>();
		for (int x = x1; x <= x2; x++) {
			for (int y = 0; y < scene.renderInfo.height; y++) {
				this.primaryRays.add(new Ray());
			}
		}
		System.out.println("Raytracer from " + x1 + " to " + x2
				+ " (inclusive)");
	}

	public void run() {
		synchronized (this) {
			while (true) {

				if (state.equals(RaytracerState.BUSY)) {
					if (Consts.SUBSAMPLING) {
						renderSubSampling();
					} else {
						render();
					}
					state = RaytracerState.IDLE;
					notifyAll();
				} else {
					try {
						wait();
					} catch (InterruptedException e) {
						System.out.println("Interrupted in Raytracer.run()");
					}
				}
			}
		}
	}

	public void startRender() {
		synchronized (this) {
			this.state = RaytracerState.BUSY;
			notifyAll();
		}
	}

	public void waitRender() {
		synchronized (this) {
			while (this.state.equals(RaytracerState.BUSY)) {
				try {
					wait();
				} catch (InterruptedException e) {
					return;
				}
			}
		}
	}

	public void render() {
		Ray.prepare(scene);

		image = new BufferedImage((x2 - x1) + 1, scene.renderInfo.height,
				BufferedImage.TYPE_INT_RGB);
		WritableRaster raster = image.getRaster();
		this.primaryRaysIterator = primaryRays.iterator();

		for (int x = x1; x <= x2; x++) {
			for (int y = 0; y < scene.renderInfo.height; y++) {

				if (useJitteredSuperSampling) {
					raster.setPixel(x - x1, y, jitteredSuperSample(scene, x, y)
							.asInt());
				} else {
					raster.setPixel(x - x1, y, sample(scene, x, y).asInt());
				}
			}
			// if(x % 25 == 0) {
			// System.out.println(x);
			// }
		}

		this.done = true;

	}

	public void renderSubSampling() {
		Ray.prepare(scene);

		// if(image == null) {
		image = new BufferedImage((x2 - x1) + 1, scene.renderInfo.height,
				BufferedImage.TYPE_INT_RGB);
		// }

		WritableRaster raster = image.getRaster();
		this.primaryRaysIterator = this.primaryRays.iterator();

		if (Consts.BACKGROUND) {
			image.getGraphics().drawImage(
					scene.renderInfo.getBackgroundImage(), 0, 0, (x2 - x1) + 1,
					scene.renderInfo.height, x1, 0, x2,
					scene.renderInfo.height, null);
		}

		for (int x = x1; x <= x2; x++) {
			boolean forced = false;
			int retraceYval = -1;
			int searchStep = Consts.SUBSAMPLING_STEP;
			int yStep = 1;

			for (int y = 0; y < scene.renderInfo.height; y += yStep) {

				Color col = sampleSub(scene, x, y);
				// System.out.println("y=" + y + " yStep=" + yStep + " forced="
				// + forced + " retraceYval=" + retraceYval);
				if (!col.isBackgroundHit()) {
					raster.setPixel(x - x1, y, col.asInt());

					if (!forced) {
						yStep = 1;
						retraceYval = y;
						y -= searchStep;
						if (y < 0)
							y = 0;
						forced = true;
					}
				} else {
					if (forced) {
						if (y >= retraceYval) {
							forced = false;
							y++;
						}
					} else {
						yStep = searchStep;
					}
				}
			}
		}

		this.done = true;

	}

	public boolean isDone() {
		return done;
	}

	public BufferedImage getImg() {
		return this.image;
	}

	private Color sample(Scene scene, int x, int y) {
		Ray ray = this.primaryRaysIterator.next();
		ray.init(x, y);
		IntersectionResult intersection = scene.intersect(ray);

		if (intersection != null) {
			return getColor(scene, intersection, ray, 0);
		} else {
			Color col = scene.renderInfo.getBackgroundPixel(x, y,
					scene.renderInfo.width, scene.renderInfo.height);
			col.setBackgroundHit(true);
			return col;
		}
	}

	private Color sampleSub(Scene scene, int x, int y) {
		Ray ray = this.primaryRaysIterator.next();
		ray.init(x, y);
		IntersectionResult intersection = scene.intersect(ray);

		if (intersection != null) {
			return getColor(scene, intersection, ray, 0);
		} else {
			Color col = new Color();
			col.setBackgroundHit(true);
			return col;
		}
	}

	public Scene getScene() {
		return scene;
	}

	public void setScene(Scene scene) {
		this.scene = scene;
	}

	private Color jitteredSuperSample(Scene scene, int x, int y) {
		Color col = new Color();
		float deltaXS = 1f / superSamplingValue;
		float deltaYS = 1f / superSamplingValue;

		for (int xs = 0; xs < superSamplingValue; xs++) {
			for (int ys = 0; ys < superSamplingValue; ys++) {
				Ray ray = primaryRaysIterator.next();
				ray.init(x + deltaXS * xs + getRndFloat(0, deltaXS), y
						+ deltaYS * ys + getRndFloat(0, deltaYS));

				IntersectionResult intersection = scene.intersect(ray);

				if (intersection != null) {
					col.addLocal(getColor(scene, intersection, ray, 0));
				} else {
					col.addLocal(scene.renderInfo.getBackgroundPixel(x, y,
							scene.renderInfo.width, scene.renderInfo.height));
				}
			}
		}

		col.divideLocal(superSamplingValue * superSamplingValue);
		return col;
	}

	private Color getColor(Scene scene, IntersectionResult intersection,
			Ray ray, int curDepth) {
		Color col = new Color();

		// Point lights
		if (Consts.POINTLIGHTS)
			col.addLocal(doPointLight(scene, intersection, ray));

		// Directional lights
		if (Consts.DIRLIGHTS)
			col.addLocal(doDirectionalLight(scene, intersection, ray));

		// Spot lights
		if (Consts.SPOTLIGHTS)
			col.addLocal(doSpotLight(scene, intersection, ray));

		// Ambient
		if (scene.ambient != null) {
			Color amb = new Color(scene.ambient.color);
			amb.mulLocal(intersection.mat.amb);
			col.addLocal(amb);
		}

		if (curDepth < scene.maxDepth) {

			// Reflectance
			if (!intersection.mat.spec.isZero()) {

				Ray reflectRay = null;
				if (intersection.equals(ray.lastIntersection)
						&& ray.lastIntersection.reflectiveRay != null) {
					reflectRay = ray.lastIntersection.reflectiveRay;
				} else {
					reflectRay = Ray.reflect(ray, intersection);
					intersection.reflectiveRay = reflectRay;
				}

				IntersectionResult reflectIntersect = scene
						.intersect(reflectRay);
				reflectRay.lastIntersection = reflectIntersect;

				Color reflectCol = null;
				if (reflectIntersect != null) {
					reflectCol = getColor(scene, reflectIntersect, reflectRay,
							++curDepth);
				} else {
					reflectCol = new Color(scene.renderInfo.bgCol); // scene.renderInfo.getBackgroundPixel(ray.oriX,
																	// ray.oriY,
																	// scene.renderInfo.width,
																	// scene.renderInfo.height);
				}
				reflectCol.mulLocal(intersection.mat.spec);
				col.addLocal(reflectCol);
			}

			// Refractance
			if (!intersection.mat.trans.isZero()) {
				Ray refractRay = null;
				if (intersection.equals(ray.lastIntersection)
						&& ray.lastIntersection.refractiveRay != null) {
					refractRay = ray.lastIntersection.refractiveRay;
				} else {
					refractRay = Ray.refract(ray, intersection,
							intersection.normal.dot(ray.negate()));
					intersection.refractiveRay = refractRay;
				}

				IntersectionResult refractIntersect = scene
						.intersect(refractRay);
				Color refractCol = null;

				if (refractIntersect != null) {
					refractCol = getColor(scene, refractIntersect, refractRay,
							++curDepth);
				} else {
					refractCol = new Color(scene.renderInfo.bgCol);
				}
				refractCol.mulLocal(intersection.mat.trans);
				col.addLocal(refractCol);
			}
		}
		ray.lastIntersection = intersection;
		return col;
	}

	private Color doPointLight(Scene scene, IntersectionResult intersection,
			Ray ray) {
		Color color = new Color(0f, 0f, 0f);

		for (PointLight light : scene.pointLights) {
			Vector3f towardsLight = light.pos.subtract(intersection.pos)
					.normalizeLocal();

			float lightDistSquared = light.pos.subtract(intersection.pos)
					.lengthSquared();

			IntersectionResult shadowIntersect = null;
			if (Consts.SHADOWS) {
				Ray shadowRay = new Ray(intersection.pos.add(towardsLight
						.scale(.1f)), towardsLight);
				shadowIntersect = scene.intersect(shadowRay, lightDistSquared);
			}

			if ((!(shadowIntersect != null && shadowIntersect.dist > 0 && shadowIntersect.distSquared < lightDistSquared))) {
				// if(shadowIntersect == null) {
				// Diffuse
				float kdiff = Math
						.max(0, towardsLight.dot(intersection.normal))
						/ (lightDistSquared);
				color.addLocal(intersection.mat.dif.mul(kdiff).mul(light.color));

				// Specular
				Vector3f r = intersection.normal
						.scale(2 * intersection.normal.dot(towardsLight))
						.add(towardsLight.negate()).normalizeLocal();
				float kspec = (float) Math.pow(Math.max(0, r.dot(ray)),
						intersection.mat.phong) / (lightDistSquared);
				color.addLocal(intersection.mat.spec.mul(kspec)
						.mul(light.color));
				// } else {
				// System.out.println(intersection.shape.getName());
			}
		}

		return color;
	}

	private Color doDirectionalLight(Scene scene,
			IntersectionResult intersection, Ray ray) {
		Color color = new Color(0f, 0f, 0f);

		for (DirectionalLight light : scene.directionalLights) {
			Vector3f towardsLight = light.dir.negate();

			Ray shadowRay = new Ray(intersection.pos.add(towardsLight
					.scale(.1f)), towardsLight);
			IntersectionResult shadowIntersect = scene.intersect(shadowRay,
					Float.MAX_VALUE);

			if ((!(shadowIntersect != null && shadowIntersect.dist < 0))) {
				// if(shadowIntersect == null) {
				// Diffuse
				float kdiff = Math.max(0,
						-towardsLight.dot(intersection.normal));
				color.addLocal(intersection.mat.dif.mul(kdiff).mul(light.color));

				// Specular
				Vector3f r = intersection.normal.scale(2f)
						.scale(intersection.normal.dot(towardsLight))
						.subtractLocal(towardsLight);
				float kspec = (float) Math.pow(Math.max(0, r.dot(ray)),
						intersection.mat.phong);
				color.addLocal(intersection.mat.spec.mul(kspec)
						.mul(light.color));
				// } else {
				// System.out.println(intersection.shape.getName());
			}
		}
		return color;
	}

	private Color doSpotLight(Scene scene, IntersectionResult intersection,
			Ray ray) {
		Color color = new Color(0f, 0f, 0f);

		for (SpotLight light : scene.spotLights) {
			Vector3f towardsLight = light.pos.subtract(
					intersection.pos.add(intersection.normal.scale(0.1f)))
					.normalizeLocal();

			float angleDeg = (float) (Math.acos(towardsLight.dot(light.dir)) * 360f / (float) Math.PI);
			angleDeg = (angleDeg > 180f) ? 360f - angleDeg : angleDeg;

			if (angleDeg < light.a2) {
				// float fallOff = (angleDeg > light.a1) ? 1f - (angleDeg -
				// light.a1) / light.a2 : 1f;

				float lightDistSquared = light.pos.subtract(intersection.pos)
						.lengthSquared();

				Ray shadowRay = new Ray(intersection.pos.add(towardsLight
						.scale(0.1f)), towardsLight);
				IntersectionResult shadowIntersect = scene.intersect(shadowRay,
						lightDistSquared);

				if ((!(shadowIntersect != null && shadowIntersect.dist < 0 && shadowIntersect.distSquared < lightDistSquared))) {
					// Diffuse
					float kdiff = Math.max(0,
							towardsLight.dot(intersection.normal))
							/ (lightDistSquared);
					color.addLocal(intersection.mat.dif.mul(kdiff).mul(
							light.color));

					// Specular
					Vector3f r = intersection.normal
							.scale(2 * intersection.normal.dot(towardsLight))
							.add(towardsLight.negate()).normalizeLocal();
					float kspec = (float) Math.pow(Math.max(0, r.dot(ray)),
							intersection.mat.phong) / (lightDistSquared);
					color.addLocal(intersection.mat.spec.mul(kspec).mul(
							light.color));
				}
			}
		}
		return color;
	}

	public float getRndFloat(float min, float max) {
		return (this.rnd.nextFloat() * (max - min)) + min;
	}

}
